<?php declare(strict_types=1);

/**
 * @package PluginMaker (Light Portal)
 * @link https://custom.simplemachines.org/index.php?mod=4244
 * @author Bugo <bugo@dragomano.ru>
 * @copyright 2021-2025 Bugo
 * @license https://spdx.org/licenses/GPL-3.0-or-later.html GPL-3.0-or-later
 *
 * @category plugin
 * @version 17.10.25
 */

namespace LightPortal\Plugins\PluginMaker;

use Bugo\Compat\Config;
use Bugo\Compat\Lang;
use Bugo\Compat\User;
use Bugo\Compat\Utils;
use LightPortal\Enums\PluginType;
use LightPortal\Enums\PortalHook;
use LightPortal\Plugins\Block;
use LightPortal\Plugins\Editor;
use LightPortal\Plugins\Event;
use LightPortal\Plugins\GameBlock;
use LightPortal\Plugins\Plugin;
use LightPortal\Plugins\PluginAttribute;
use LightPortal\Plugins\SsiBlock;
use LightPortal\UI\Fields\CheckboxField;
use LightPortal\UI\Fields\ColorField;
use LightPortal\UI\Fields\CustomField;
use LightPortal\UI\Fields\NumberField;
use LightPortal\UI\Fields\RadioField;
use LightPortal\UI\Fields\RangeField;
use LightPortal\UI\Fields\SelectField;
use LightPortal\UI\Fields\TextField;
use LightPortal\Utils\Setting;
use Nette\PhpGenerator\ClassType;
use Nette\PhpGenerator\Literal;
use Nette\PhpGenerator\Method;
use Nette\PhpGenerator\PhpFile;
use Nette\PhpGenerator\PhpNamespace;
use Nette\PhpGenerator\Printer;

use const LP_NAME;

if (! defined('LP_NAME'))
	die('No direct access...');

class Generator
{
	private readonly PhpNamespace $namespace;

	private readonly ClassType $class;

	public function __construct(private array $plugin)
	{
		require_once __DIR__ . '/vendor/autoload.php';

		$this->namespace = $this->getNamespace();

		$attributes = [];

		if ($type = $this->getPluginType()) {
			$attributes['type'] = $type;
		}

		if ($icon = $this->plugin['icon']) {
			$attributes['icon'] = $icon;
		}

		$class = ($this->namespace->addClass($this->plugin['name']))
			->addComment('Generated by PluginMaker')
			->setExtends($this->getExtendedClass());

		if (! empty($attributes)) {
			$class->addAttribute(PluginAttribute::class, $attributes);
		}

		$this->class = $class;
	}

	public function generate(): void
	{
		$this->addProperties();
		$this->addFrontLayoutsMethod();
		$this->addCustomLayoutExtensionsMethod();
		$this->addInitMethod();
		$this->addUpdateAdminAreasMethod();
		$this->addPrepareBlockParamsMethod();
		$this->addValidateBlockParamsMethod();
		$this->addPrepareBlockFieldsMethod();
		$this->addPreparePageParamsMethod();
		$this->addValidatePageParamsMethod();
		$this->addPreparePageFieldsMethod();
		$this->handleOptions();
		$this->addAddSettingsMethod();
		$this->addParseContentMethod();
		$this->addPrepareContentMethod();
		$this->addPrepareEditorMethod();
		$this->addGetSupportedContentTypesMethod();
		$this->addCommentsMethod();
		$this->addCreditsMethod();
		$this->createFile();
	}

	private function getNamespace(): PhpNamespace
	{
		$namespace = new PhpNamespace('LightPortal\Plugins\\' . $this->plugin['name']);
		$namespace->addUse($this->getExtendedClass());
		$namespace->addUse(Config::class);
		$namespace->addUse(Lang::class);
		$namespace->addUse(User::class);
		$namespace->addUse(Utils::class);
		$namespace->addUse(Event::class);
		$namespace->addUse(PluginAttribute::class);
		$namespace->addUse(PluginType::class);
		$namespace->addUse(PortalHook::class);

		if ($this->hasType(PluginType::COMMENT)) {
			$namespace->addUse(Setting::class);
		}

		return $namespace;
	}

	private function getPluginType(): Literal|array|null
	{
		$excludes = [
			PluginType::BLOCK->name(),
			PluginType::EDITOR->name(),
			PluginType::GAMES->name(),
			PluginType::OTHER->name(),
			PluginType::SSI->name(),
		];

		$filteredTypes = array_values(array_filter(
			$this->plugin['types'],
			fn($type) => ! in_array($type, $excludes)
		));

		if (empty($filteredTypes)) {
			return null;
		}

		$parsedTypes = array_map(
			fn($type) => new Literal('PluginType::' . strtoupper($type)),
			$filteredTypes
		);

		return count($parsedTypes) === 1 ? $parsedTypes[0] : $parsedTypes;
	}

	private function getExtendedClass(): string
	{
		return match (true) {
			$this->hasType(PluginType::GAMES)  => GameBlock::class,
			$this->hasType(PluginType::SSI)    => SsiBlock::class,
			$this->hasType(PluginType::BLOCK)  => Block::class,
			$this->hasType(PluginType::EDITOR) => Editor::class,
			default => Plugin::class,
		};
	}

	private function addProperties(): void
	{
		if ($this->hasType(PluginType::FRONTPAGE)) {
			$this->class
				->addProperty('showSaveButton', false)
				->setType('bool');

			$this->class
				->addProperty('extension', '.ext')
				->setPrivate()
				->setType('string');
		}
	}

	private function addFrontLayoutsMethod(): void
	{
		if (! $this->hasType(PluginType::FRONTPAGE))
			return;

		$this->class
			->addMethod(PortalHook::frontLayouts->name)
			->setReturnType('void')
			->addBody("if (! str_contains(Config::\$modSettings['lp_frontpage_layout'], \$this->extension))")
			->addBody("\treturn;" . PHP_EOL)
			->addBody("require_once __DIR__ . '/vendor/autoload.php';" . PHP_EOL)
			->addBody("ob_start();" . PHP_EOL)
			->addBody("// Add your code here" . PHP_EOL)
			->addBody("Utils::\$context['lp_layout'] = ob_get_clean();" . PHP_EOL)
			->addBody("Config::\$modSettings['lp_frontpage_layout'] = '';" . PHP_EOL);
	}

	private function addCustomLayoutExtensionsMethod(): void
	{
		if (! $this->hasType(PluginType::FRONTPAGE))
			return;

		$this->class
			->addMethod(PortalHook::layoutExtensions->name)
			->setReturnType('void')
			->setBody("\$e->args->extensions[] = \$this->extension;")
			->addParameter('e')
			->setType(Event::class);
	}

	private function addInitMethod(): void
	{
		$method = $this->class
			->addMethod(PortalHook::init->name)
			->setReturnType('void');

		match (true) {
			$this->hasType(PluginType::PARSER) => $method->setBody(
				"Utils::\$context['lp_content_types'][\$this->name] = '" . $this->plugin['name'] . "';"
			),
			$this->hasType(PluginType::COMMENT) => $method->setBody(
				"Lang::\$txt['lp_comment_block_set'][\$this->name] = '" . $this->plugin['name'] . "';"
			),
			! empty($this->plugin['smf_hooks']) => $method->setBody(
				"// \$this->applyHook('hook_name');"
			),
			default => null
		};
	}

	private function addUpdateAdminAreasMethod(): void
	{
		if (! $this->hasType(PluginType::IMPEX))
			return;

		$method = $this->class
			->addMethod(PortalHook::extendAdminAreas->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method->addBody("// Check out the TinyPortalMigration plugin as an example");
	}

	private function addPrepareBlockParamsMethod(): void
	{
		if (! $this->hasType([PluginType::BLOCK, PluginType::BLOCK_OPTIONS]))
			return;

		$method = $this->class
			->addMethod(PortalHook::prepareBlockParams->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		if (empty($blockParams = $this->getSpecialParams())) {
			$method->addBody("// TODO: Implement prepareBlockParams() method." . PHP_EOL);
			return;
		}

		$method->addBody("\$e->args->params = [");

		foreach ($blockParams as $param) {
			$method->addBody("\t'{$param['name']}' => {$this->getDefaultValue($param)},");
		}

		$method->addBody("];");
	}

	private function addValidateBlockParamsMethod(): void
	{
		if (! $this->hasType([PluginType::BLOCK, PluginType::BLOCK_OPTIONS]))
			return;

		$method = $this->class
			->addMethod(PortalHook::validateBlockParams->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		if (empty($blockParams = $this->getSpecialParams())) {
			$method->addBody("// TODO: Implement validateBlockParams() method." . PHP_EOL);
			return;
		}

		$method->addBody("\$e->args->params = [");

		foreach ($blockParams as $param) {
			$method->addBody("\t'{$param['name']}' => {$this->getFilter($param)},");
		}

		$method->addBody("];");
	}

	private function addPrepareBlockFieldsMethod(): void
	{
		if (! $this->hasType([PluginType::BLOCK, PluginType::BLOCK_OPTIONS]))
			return;

		$method = $this->class
			->addMethod(PortalHook::prepareBlockFields->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method->addBody("// TODO: Implement prepareBlockFields() method." . PHP_EOL);

		$this->prepareFields($method);
	}

	private function addPreparePageParamsMethod(): void
	{
		if (! $this->hasType(PluginType::PAGE_OPTIONS))
			return;

		$method = $this->class
			->addMethod(PortalHook::preparePageParams->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		if (empty($pageParams = $this->getSpecialParams('page'))) {
			$method->addBody("// TODO: Implement preparePageParams() method." . PHP_EOL);
			return;
		}

		foreach ($pageParams as $param) {
			$method->addBody("\$e->args->params['{$param['name']}'] = {$this->getDefaultValue($param)};");
		}
	}

	private function addValidatePageParamsMethod(): void
	{
		if (! $this->hasType(PluginType::PAGE_OPTIONS))
			return;

		$method = $this->class
			->addMethod(PortalHook::validatePageParams->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		if (empty($pageParams = $this->getSpecialParams('page'))) {
			$method->addBody("// TODO: Implement validatePageParams() method." . PHP_EOL);
			return;
		}

		$method->addBody("\$e->args->params += [");

		foreach ($pageParams as $param) {
			$method->addBody("\t'{$param['name']}' => {$this->getFilter($param)},");
		}

		$method->addBody("];");
	}

	private function addPreparePageFieldsMethod(): void
	{
		if (! $this->hasType(PluginType::PAGE_OPTIONS))
			return;

		$method = $this->class
			->addMethod(PortalHook::preparePageFields->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method->addBody("// TODO: Implement preparePageFields() method." . PHP_EOL);

		$this->prepareFields($method, 'page');
	}

	private function addAddSettingsMethod(): void
	{
		if (empty($this->plugin['options']))
			return;

		$method = $this->class
			->addMethod(PortalHook::addSettings->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$defaultOptions = array_filter(
			$this->plugin['options'],
			static fn($optionArray) => array_key_exists('default', $optionArray)
		);

		if (! empty($defaultOptions)) {
			$method->addBody("\$this->addDefaultValues([");

			foreach ($defaultOptions as $option) {
				$method->addBody("\t'{$option['name']}' => {$this->getDefaultValue($option)},");
			}

			$method->addBody("]);" . PHP_EOL);
		}

		foreach ($this->plugin['options'] as $option) {
			if (in_array($option['type'], ['multiselect', 'select'])) {
				$method
					->addBody("\$e->args->settings[\$this->name][] = ['{$option['type']}', '{$option['name']}', \$this->txt['{$option['name']}_set']];");
			} else {
				$method
					->addBody("\$e->args->settings[\$this->name][] = ['{$option['type']}', '{$option['name']}'];");
			}
		}
	}

	private function addParseContentMethod(): void
	{
		if (! $this->hasType(PluginType::PARSER))
			return;

		$method = $this->class
			->addMethod(PortalHook::parseContent->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method->addBody("\$e->args->content = \$this->getParsedContent(\$e->args->content);" . PHP_EOL);

		$method = $this->class
			->addMethod('getParsedContent')
			->setReturnType('string');

		$method
			->addParameter('text')
			->setType('string');

		$method->addBody("return '';");
	}

	private function addPrepareContentMethod(): void
	{
		if (! $this->hasType([PluginType::BLOCK, PluginType::SSI, PluginType::GAMES]))
			return;

		$method = $this->class
			->addMethod(PortalHook::prepareContent->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		if ($this->hasType(PluginType::SSI)) {
			$method
				->addBody("\$data = \$this->getFromSSI('recentTopics', 10, [], [], 'array');" . PHP_EOL)
				->addBody("var_dump(\$data);");
		} else {
			$method->addBody("echo 'Add your html code here';");
		}
	}

	private function addPrepareEditorMethod(): void
	{
		if (! $this->hasType(PluginType::EDITOR))
			return;

		$method = $this->class
			->addMethod(PortalHook::prepareEditor->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method
			->addBody("if (! \$this->isContentSupported(\$e->args->object))")
			->addBody("\treturn;" . PHP_EOL)
			->addBody("// TODO: Add your editor initialization code here");
	}

	private function addGetSupportedContentTypesMethod(): void
	{
		if (! $this->hasType(PluginType::EDITOR))
			return;

		$this->class
			->addMethod('getSupportedContentTypes')
			->setReturnType('array')
			->setProtected()
			->addBody("// TODO: Return an array of supported content types, e.g., ['html', 'markdown']");
	}

	private function addCommentsMethod(): void
	{
		if (! $this->hasType(PluginType::COMMENT))
			return;

		$method = $this->class
			->addMethod(PortalHook::comments->name)
			->setReturnType('void');

		$method
			->addBody("if (Setting::getCommentBlock() !== \$this->name)")
			->addBody("\treturn;" . PHP_EOL)
			->addBody("// TODO: Implement comments() method.");
	}

	private function addCreditsMethod(): void
	{
		if (empty($this->plugin['components']))
			return;

		$method = $this->class
			->addMethod(PortalHook::credits->name)
			->setReturnType('void');

		$method
			->addParameter('e')
			->setType(Event::class);

		$method
			->addBody("\$e->args->links[] = [")
			->addBody("\t'title' => '" . Lang::$txt['lp_plugin_maker']['component_name'] . "',")
			->addBody("\t'link' => '" . Lang::$txt['lp_plugin_maker']['component_link'] . "',")
			->addBody("\t'author' => '" . Lang::$txt['lp_plugin_maker']['component_author'] . "',")
			->addBody("\t'license' => [")
			->addBody("\t\t'name' => '" . Lang::$txt['lp_plugin_maker']['license_name'] . "',")
			->addBody("\t\t'link' => '" . Lang::$txt['lp_plugin_maker']['license_link'] . "'")
			->addBody("\t]")
			->addBody("];");
	}

	private function createFile(): void
	{
		$file = new PhpFile;
		$file->addNamespace($this->namespace);

		$this->addDocBlock($file);

		$printer = new class extends Printer {};
		$printer->linesBetweenProperties = 1;
		$printer->linesBetweenMethods = 1;

		$content = $printer->printFile($file);

		$plugin = new Builder($this->plugin['name']);
		$plugin->create($content);

		$this->createLanguages($plugin);
	}

	private function getSpecialParams(string $type = 'block'): array
	{
		$params = [];
		$this->plugin[$type . '_options'] = [];
		foreach ($this->plugin['options'] as $id => $option) {
			if (str_contains((string) $option['name'], $type . '_')) {
				$option['name'] = str_replace($type . '_', '', (string) $option['name']);
				$params[] = $option;
				$this->plugin[$type . '_options'][$id] = $option;
			}
		}

		return $params;
	}

	private function getDefaultValue(array $option): string
	{
		$default = match ($option['type']) {
			'int'   => (int) $option['default'],
			'float' => (float) $option['default'],
			default => $option['default'],
		};

		return var_export($default, true);
	}

	private function getFilter(array $param): string
	{
		return match ($param['type']) {
			'url' => 'FILTER_VALIDATE_URL',
			'int', 'range' => 'FILTER_VALIDATE_INT',
			'float' => 'FILTER_VALIDATE_FLOAT',
			'check' => 'FILTER_VALIDATE_BOOLEAN',
			default => 'FILTER_DEFAULT',
		};
	}

	private function handleOptions(): void
	{
		$this->plugin['options'] = array_diff_key(
			$this->plugin['options'] ?? [],
			$this->plugin[PluginType::BLOCK_OPTIONS->name()] ?? [],
			$this->plugin[PluginType::PAGE_OPTIONS->name()] ?? []
		);
	}

	private function prepareFields(Method $method, string $type = 'block'): void
	{
		if (empty($params = $this->getSpecialParams($type)))
			return;

		foreach ($params as $param) {
			if ($param['type'] === 'text') {
				$this->namespace->addUse(TextField::class);

				$method
					->addBody("TextField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'url') {
				$this->namespace->addUse(TextField::class);

				$method
					->addBody("TextField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setType('url')")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'check') {
				$this->namespace->addUse(CheckboxField::class);

				$method
					->addBody("CheckboxField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'color') {
				$this->namespace->addUse(ColorField::class);

				$method
					->addBody("ColorField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'int') {
				$this->namespace->addUse(NumberField::class);

				$method
					->addBody("NumberField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'float') {
				$this->namespace->addUse(NumberField::class);

				$method
					->addBody("NumberField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setAttribute('step', 0.1)")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'select') {
				$this->namespace->addUse(RadioField::class);

				$method
					->addBody("RadioField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setOptions(\$this->txt['{$param['name']}_set'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'multiselect') {
				$this->namespace->addUse(SelectField::class);

				$method
					->addBody("SelectField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setOptions(\$this->txt['{$param['name']}_set'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if ($param['type'] === 'range') {
				$this->namespace->addUse(RangeField::class);

				$method
					->addBody("RangeField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(Utils::\$e->args->options['{$param['name']}']);" . PHP_EOL);
			}

			if (in_array($param['type'], ['title', 'desc', 'callback'])) {
				$this->namespace->addUse(CustomField::class);

				$method
					->addBody("CustomField::make('{$param['name']}', \$this->txt['{$param['name']}'])")
					->addBody("\t->setValue(static fn() => '', []);" . PHP_EOL);
			}
		}
	}

	private function addDocBlock(PhpFile $file): void
	{
		$licenseEnum = License::tryFrom($this->plugin['license']);

		if ($licenseEnum) {
			$licenseName = $licenseEnum->getName();
			$licenseLink = $licenseEnum->getLink();
		} else {
			$licenseName = Lang::$txt['lp_plugin_maker']['license_name'];
			$licenseLink = Lang::$txt['lp_plugin_maker']['license_link'];
		}

		$file->addComment("@package " . $this->plugin['name'] . " (" . LP_NAME .')');
		$file->addComment("@link " . $this->plugin['site']);
		$file->addComment("@author " . $this->plugin['author'] . " <" . $this->plugin['email'] . ">");
		$file->addComment("@copyright " . date('Y') . " " . $this->plugin['author']);
		$file->addComment(sprintf('@license %s %s', $licenseLink, $licenseName));
		$file->addComment('');
		$file->addComment("@category plugin");
		$file->addComment("@version " . date('d.m.y'));
	}

	private function createLanguages(Builder $plugin): void
	{
		if (empty($this->plugin['descriptions']))
			return;

		$languages = [];

		foreach ($this->plugin['descriptions'] as $lang => $value) {
			$languages[$lang][] = '<?php' . PHP_EOL . PHP_EOL;
			$languages[$lang][] = 'return [';

			if ($this->hasType(PluginType::BLOCK)) {
				$title = $this->plugin['titles'][$lang] ?? $this->plugin['name'];
				$languages[$lang][] = PHP_EOL . "\t'title' => '$title',";
			}

			$languages[$lang][] = PHP_EOL . "\t'description' => '$value',";
		}

		$this->plugin['options'] = array_merge(
			$this->plugin['options'] ?? [],
			$this->plugin[PluginType::BLOCK_OPTIONS->name()] ?? [],
			$this->plugin[PluginType::PAGE_OPTIONS->name()] ?? [],
		);

		foreach ($this->plugin['options'] as $option) {
			foreach ($option['translations'] as $lang => $value) {
				if (empty($languages[$lang]))
					continue;

				$languages[$lang][] = PHP_EOL . "\t'{$option['name']}' => '$value',";

				if (in_array($option['type'], ['multiselect', 'select'])) {
					if (! empty($option['variants'])) {
						$variants  = explode('|', (string) $option['variants']);
						$variants = "'" . implode("','", $variants) . "'";

						$languages[$lang][] = PHP_EOL . "\t'{$option['name']}_set' => [$variants],";
					}
				}
			}
		}

		foreach ($this->plugin['descriptions'] as $lang => $dump) {
			$languages[$lang][] = PHP_EOL . '];' . PHP_EOL;
		}

		$plugin->createLangs($languages);
	}

	private function hasType(PluginType|array $type): bool
	{
		$types = is_array($type) ? $type : [$type];
		$typeNames = array_map(fn(PluginType $t) => $t->name(), $types);

		return (bool) array_intersect($typeNames, $this->plugin['types']);
	}
}
